FlureeLabs
GuideMarch 30, 2026

Coming from Apache Jena

How to move from Jena/Fuseki to Fluree. What carries over, what changes, and what Fluree does that Jena doesn't.

MigrationSPARQLJena

SPARQL compatibility

Fluree implements SPARQL 1.1. Existing queries that run against Fuseki work without modification.

Supported:

  • All query forms: SELECT, CONSTRUCT, ASK, DESCRIBE
  • All graph patterns: FILTER, OPTIONAL, UNION, MINUS, EXISTS, BIND, VALUES
  • Solution modifiers: GROUP BY (including expression-based), HAVING, ORDER BY, LIMIT, OFFSET
  • Aggregations: COUNT, SUM, AVG, MIN, MAX, SAMPLE, GROUP_CONCAT (all with DISTINCT)
  • Property paths: +, *, ^, |, / (sequences, alternatives, inverse, transitive)
  • Subqueries
  • SPARQL UPDATE: INSERT DATA, DELETE DATA, DELETE/INSERT WHERE, DELETE WHERE, MODIFY with WITH/USING/USING NAMED
  • All standard functions: string, numeric, date/time, type conversion, logical, XSD casts

The query endpoint accepts Content-Type: application/sparql-query:

curl -X POST http://localhost:8090/v1/fluree/query \
  -H "Content-Type: application/sparql-query" \
  -d '
    PREFIX schema: <http://schema.org/>

    SELECT ?name ?email
    FROM <mydb:main>
    WHERE {
      ?person a schema:Person .
      ?person schema:name ?name .
      ?person schema:email ?email .
    }
    ORDER BY ?name
  '

SPARQL UPDATE uses Content-Type: application/sparql-update and targets the update endpoint:

curl -X POST http://localhost:8090/v1/fluree/update/mydb:main \
  -H "Content-Type: application/sparql-update" \
  -d '
    PREFIX ex: <http://example.org/ns/>

    INSERT DATA {
      ex:alice ex:name "Alice" .
      ex:alice ex:age 30 .
    }
  '

What changes

Installation

Jena requires a JVM. Fluree is a single native binary written in Rust — no runtime dependencies.

# Download
curl -L https://github.com/fluree/db-rust/releases/latest/download/fluree-server-linux -o fluree-server
chmod +x fluree-server

# Or Docker
docker run -p 8090:8090 fluree/fluree-server:latest

Start with persistent storage:

./fluree-server --storage-path /var/lib/fluree --indexing-enabled

Ledger creation

Jena uses TDB2 datasets configured at the filesystem level. Fluree uses ledgers. Create one before loading data:

curl -X POST http://localhost:8090/v1/fluree/create \
  -H "Content-Type: application/json" \
  -d '{"ledger": "mydb:main"}'

Ledger IDs have the format name:branch. The default branch is main, so mydb and mydb:main are equivalent.

Loading Turtle files

Load existing Turtle data via the HTTP API:

# Insert (pure insert, fast path)
curl -X POST "http://localhost:8090/v1/fluree/insert?ledger=mydb:main" \
  -H "Content-Type: text/turtle" \
  --data-binary '@your-data.ttl'

# Upsert (idempotent — retracts existing values for supplied predicates, then asserts new ones)
curl -X POST "http://localhost:8090/v1/fluree/upsert?ledger=mydb:main" \
  -H "Content-Type: text/turtle" \
  --data-binary '@your-data.ttl'

TriG files (named graphs) are supported on the upsert endpoint with Content-Type: application/trig.

For large files, split into batches of 10,000–100,000 triples and allow indexing time between batches.

Dataset specification

In Fuseki, the dataset is implicit — configured per endpoint. In Fluree, specify the ledger in the query using FROM:

-- Fuseki (dataset implicit in endpoint URL)
SELECT ?s ?p ?o WHERE { ?s ?p ?o }

-- Fluree (dataset explicit in FROM)
SELECT ?s ?p ?o
FROM <mydb:main>
WHERE { ?s ?p ?o }

Or use the ledger-scoped endpoint: POST /v1/fluree/query/mydb:main.


What Fluree does that Jena doesn't

Immutable history and time travel

Fluree is an append-only database. Transactions don't overwrite previous state — they add new facts and record retractions of old ones. Every transaction gets a monotonically increasing t value and an ISO timestamp.

Query any historical state by adding a time specifier to the FROM clause:

-- State at transaction 100
SELECT ?name ?age
FROM <mydb:main@t:100>
WHERE { ?person ex:name ?name . ?person ex:age ?age . }

-- State at a specific datetime
SELECT ?name
FROM <mydb:main@iso:2024-06-15T10:00:00Z>
WHERE { ?person ex:name ?name . }

-- State at a specific commit (content-addressed hash)
SELECT ?name
FROM <mydb:main@commit:bafybeig...>
WHERE { ?person ex:name ?name . }

Track all changes to an entity over time using RDF-star syntax with a FROM...TO range:

PREFIX f: <https://ns.flur.ee/db#>

SELECT ?name ?t ?op
FROM <mydb:main@t:1>
TO <mydb:main@t:latest>
WHERE {
  << ex:alice ex:name ?name >> f:t ?t .
  << ex:alice ex:name ?name >> f:op ?op .
}
ORDER BY ?t

Each row shows the value, the transaction time, and whether it was an "assert" or "retract".

In Jena, updates overwrite previous state. There is no built-in mechanism for historical queries.

Branching

Fluree ledgers support branching. A branch starts as a snapshot of the source branch and is fully isolated — transactions on one branch are invisible to the other.

fluree branch create dev --ledger mydb

curl -X POST "http://localhost:8090/v1/fluree/insert?ledger=mydb:dev" \
  -H "Content-Type: text/turtle" \
  --data-binary '@experimental-data.ttl'

fluree branch rebase dev --strategy take-both

Conflict resolution operates at the (subject, predicate, graph) tuple level. Strategies: take-both, take-source, take-branch, abort, skip.

Cross-ledger queries

Jena's TDB2 doesn't natively join across separate datasets. Fluree supports cross-ledger queries using SERVICE with the fluree:ledger: URI scheme:

PREFIX ex: <http://example.org/ns/>

SELECT ?customerName ?orderTotal
FROM <customers:main>
FROM NAMED <orders:main>
WHERE {
  ?customer ex:name ?customerName .
  SERVICE <fluree:ledger:orders:main> {
    ?order ex:customer ?customer .
    ?order ex:total ?orderTotal .
  }
}

The target ledger must be included in the dataset via FROM or FROM NAMED.

Vectors are stored using the @vector datatype (f32 arrays, SIMD-accelerated) and queried with inline similarity functions:

{
  "@context": { "ex": "http://example.org/", "f": "https://ns.flur.ee/db#" },
  "select": ["?doc", "?score"],
  "values": [["?queryVec"], [{"@value": [0.88, 0.12, 0.08], "@type": "f:embeddingVector"}]],
  "where": [
    {"@id": "?doc", "ex:embedding": "?vec"},
    ["bind", "?score", "(cosineSimilarity ?vec ?queryVec)"]
  ],
  "orderBy": [["desc", "?score"]],
  "limit": 10
}

Three functions: cosineSimilarity, dotProduct, euclideanDistance. Inline functions do a flat scan with no index setup. For >100K vectors, HNSW indexes (embedded usearch) provide O(log n) ANN search.

Jena has no vector search.

Full-text search (BM25)

BM25 ranking with Block-Max WAND pruning, built in as a graph source. Jena requires configuring Lucene integration separately.

Content-addressed storage

Every commit is content-addressed with a CID. Combined with W3C Verifiable Credentials and DID support for transaction signing, database states can be cryptographically verified.

Triple-level access control

Access policies evaluated at query time on individual triples. Jena has no built-in data-level access control.

Encryption at rest

AES-256-GCM transparent encryption. Jena has no built-in encryption.

JSON-LD query language

In addition to SPARQL, Fluree provides a JSON-based query language. Both compile to the same IR and share the execution engine:

{
  "@context": {"schema": "http://schema.org/"},
  "from": "mydb:main",
  "select": ["?name", "?email"],
  "where": [
    {"@id": "?person", "schema:name": "?name"},
    {"@id": "?person", "schema:email": "?email"}
  ]
}

Migration steps

  1. Install Fluree — download binary, Docker, or build from source.
  2. Start the server: ./fluree-server --storage-path /var/lib/fluree --indexing-enabled
  3. Create a ledger: POST /v1/fluree/create with {"ledger": "mydb:main"}
  4. Load Turtle data: POST /v1/fluree/insert with Content-Type: text/turtle
  5. Add FROM <mydb:main> to existing SPARQL queries.
  6. Verify results match.

Performance reference

SPARQLoscope DBLP evaluation (105 queries):

EngineGeo MeanArith MeanFailed Queries
Fluree0.28s0.95s0%
Jena15.29s94.21s18.1%

Full results and methodology in the benchmark report.