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.
Vector search
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
- Install Fluree — download binary, Docker, or build from source.
- Start the server:
./fluree-server --storage-path /var/lib/fluree --indexing-enabled - Create a ledger:
POST /v1/fluree/createwith{"ledger": "mydb:main"} - Load Turtle data:
POST /v1/fluree/insertwithContent-Type: text/turtle - Add
FROM <mydb:main>to existing SPARQL queries. - Verify results match.
Performance reference
SPARQLoscope DBLP evaluation (105 queries):
| Engine | Geo Mean | Arith Mean | Failed Queries |
|---|---|---|---|
| Fluree | 0.28s | 0.95s | 0% |
| Jena | 15.29s | 94.21s | 18.1% |
Full results and methodology in the benchmark report.