Concepts mapping
| Neo4j | Fluree |
|---|---|
| Database | Ledger (mydb:main) |
| Node | Entity (identified by IRI, e.g., ex:alice) |
| Label | rdf:type / @type |
| Property | Predicate (e.g., schema:name) |
| Relationship | Object property (IRI reference to another entity) |
| Node ID (internal integer) | IRI (user-defined, stable, meaningful) |
| Cypher | SPARQL 1.1 or JSON-LD Query |
| APOC procedures | Graph sources (BM25, vector, Iceberg) |
Key difference: Neo4j node IDs are internal integers that can change across exports. Fluree entities are identified by IRIs that you control and that remain stable.
Query languages
Fluree provides two query languages. Both compile to the same internal IR and share the execution engine.
JSON-LD Query
JSON-based, pattern-matching query language:
{
"@context": {"ex": "http://example.org/ns/", "schema": "http://schema.org/"},
"from": "mydb:main",
"select": ["?personName", "?companyName"],
"where": [
{"@id": "?person", "@type": "schema:Person", "schema:name": "?personName"},
{"@id": "?person", "schema:worksFor": "?company"},
{"@id": "?company", "schema:name": "?companyName"},
{"@id": "?person", "schema:age": "?age"}
],
"filter": "?age > 25",
"orderBy": ["?personName"]
}
SPARQL
W3C standard RDF query language:
PREFIX schema: <http://schema.org/>
SELECT ?personName ?companyName
FROM <mydb:main>
WHERE {
?person a schema:Person ;
schema:name ?personName ;
schema:worksFor ?company ;
schema:age ?age .
?company schema:name ?companyName .
FILTER (?age > 25)
}
ORDER BY ?personName
Common Cypher → Fluree translations
Create nodes
Cypher:
CREATE (a:Person {name: "Alice", age: 30, email: "alice@example.org"})
Fluree (JSON-LD insert):
curl -X POST "http://localhost:8090/v1/fluree/insert?ledger=mydb:main" \
-H "Content-Type: application/json" \
-d '{
"@context": {"ex": "http://example.org/ns/", "schema": "http://schema.org/"},
"@graph": [{
"@id": "ex:alice",
"@type": "schema:Person",
"schema:name": "Alice",
"schema:age": 30,
"schema:email": "alice@example.org"
}]
}'
Fluree (SPARQL UPDATE):
PREFIX schema: <http://schema.org/>
PREFIX ex: <http://example.org/ns/>
INSERT DATA {
ex:alice a schema:Person ;
schema:name "Alice" ;
schema:age 30 ;
schema:email "alice@example.org" .
}
Create relationships
Cypher:
MATCH (a:Person {name: "Alice"}), (c:Company {name: "Acme"})
CREATE (a)-[:WORKS_FOR]->(c)
Fluree:
{
"@context": {"ex": "http://example.org/ns/", "schema": "http://schema.org/"},
"@graph": [{"@id": "ex:alice", "schema:worksFor": {"@id": "ex:acme"}}]
}
Relationships are properties whose values are IRIs ({"@id": "..."}) rather than literals.
Update properties
Cypher:
MATCH (a:Person {name: "Alice"}) SET a.age = 31
Fluree (update endpoint):
{
"@context": {"ex": "http://example.org/ns/", "schema": "http://schema.org/"},
"where": [{"@id": "ex:alice", "schema:age": "?oldAge"}],
"delete": [{"@id": "ex:alice", "schema:age": "?oldAge"}],
"insert": [{"@id": "ex:alice", "schema:age": 31}]
}
Fluree (upsert — simpler, replaces values for supplied predicates):
curl -X POST "http://localhost:8090/v1/fluree/upsert?ledger=mydb:main" \
-H "Content-Type: application/json" \
-d '{"@context": {"schema": "http://schema.org/", "ex": "http://example.org/ns/"}, "@graph": [{"@id": "ex:alice", "schema:age": 31}]}'
Variable-length path traversal
Cypher:
MATCH (a:Person {name: "Alice"})-[:KNOWS*1..]->(friend)
RETURN friend.name
Fluree (SPARQL property paths):
PREFIX ex: <http://example.org/ns/>
SELECT ?friendName
FROM <mydb:main>
WHERE {
ex:alice ex:knows+ ?friend .
?friend ex:name ?friendName .
}
Supported path operators: + (one or more), * (zero or more), ^ (inverse), | (alternative), / (sequence).
Aggregation
Cypher:
MATCH (p:Person)
RETURN p.department, count(p) AS count, avg(p.age) AS avgAge
ORDER BY count DESC
Fluree (SPARQL):
PREFIX ex: <http://example.org/ns/>
SELECT ?department (COUNT(?person) AS ?count) (AVG(?age) AS ?avgAge)
FROM <mydb:main>
WHERE {
?person a ex:Person ;
ex:department ?department ;
ex:age ?age .
}
GROUP BY ?department
ORDER BY DESC(?count)
Optional properties
Cypher:
MATCH (p:Person)
OPTIONAL MATCH (p)-[:HAS_PHONE]->(phone)
RETURN p.name, phone.number
Fluree (SPARQL):
PREFIX ex: <http://example.org/ns/>
SELECT ?name ?phone
FROM <mydb:main>
WHERE {
?person ex:name ?name .
OPTIONAL { ?person ex:phone ?phone }
}
Exporting data from Neo4j
Option 1: Neosemantics (n10s) plugin
The Neosemantics plugin exports Neo4j data as RDF:
CALL n10s.rdf.export.cypher("MATCH (n) RETURN n", "Turtle")
This produces Turtle that can be loaded directly into Fluree.
Option 2: APOC JSON export + conversion
CALL apoc.export.json.all("export.json", {})
Then convert to JSON-LD by mapping node labels to @type, properties to predicates with namespace prefixes, and relationships to {"@id": "..."} references.
Loading into Fluree
# Start Fluree
docker run -p 8090:8090 -v fluree-data:/data \
-e FLUREE_STORAGE_PATH=/data \
fluree/fluree-server:latest
# Create a ledger
curl -X POST http://localhost:8090/v1/fluree/create \
-H "Content-Type: application/json" \
-d '{"ledger": "mydb:main"}'
# Load Turtle export
curl -X POST "http://localhost:8090/v1/fluree/upsert?ledger=mydb:main" \
-H "Content-Type: text/turtle" \
--data-binary '@neo4j-export.ttl'
# Or load JSON-LD
curl -X POST "http://localhost:8090/v1/fluree/insert?ledger=mydb:main" \
-H "Content-Type: application/json" \
-d @neo4j-export.jsonld
What Fluree does that Neo4j doesn't
Time travel
Every transaction is immutable. Query any historical state:
{
"from": "mydb:main@iso:2024-06-15T14:30:00Z",
"select": ["?name", "?email"],
"where": [{"@id": "ex:alice", "schema:name": "?name", "schema:email": "?email"}]
}
Track all changes over time with FROM...TO and @t/@op annotations:
PREFIX f: <https://ns.flur.ee/db#>
SELECT ?age ?t ?op
FROM <mydb:main@t:1>
TO <mydb:main@t:latest>
WHERE {
<< ex:alice ex:age ?age >> f:t ?t .
<< ex:alice ex:age ?age >> f:op ?op .
}
ORDER BY ?t
Neo4j has no temporal capability — updates overwrite previous state.
Branching
Create isolated branches, work independently, rebase when ready:
fluree branch create staging --ledger mydb
fluree branch rebase staging --strategy take-both
Conflict resolution at the (subject, predicate, graph) tuple level.
Vector search
Inline similarity functions (cosineSimilarity, dotProduct, euclideanDistance) on @vector typed values. HNSW indexes for large-scale ANN. No external service required.
Iceberg integration
Query Apache Iceberg tables as graph sources:
fluree iceberg map warehouse-orders \
--mode direct \
--table-location s3://bucket/warehouse/orders
Then join graph data with data lake data in a single query.
Content-addressed storage
Every commit has a CID. W3C Verifiable Credentials and DID support for transaction signing.
Triple-level access control
Policies evaluated at query time on individual triples.
W3C standards
Data and queries use SPARQL, RDF, JSON-LD, SHACL, OWL — all W3C standards. No proprietary query language.
Migration steps
- Export Neo4j data as RDF (Neosemantics) or JSON (APOC) and convert to JSON-LD/Turtle.
- Start Fluree:
docker run -p 8090:8090 fluree/fluree-server:latest - Create a ledger:
POST /v1/fluree/create - Load data:
POST /v1/fluree/insertor/upsert - Translate Cypher queries to SPARQL or JSON-LD Query using the patterns above.
- Verify results.