FlureeLabs
GuideMarch 30, 2026

Coming from Neo4j / Property Graphs

How to move from Neo4j to Fluree. Covers the data model differences, query translation patterns, and how to export and import data.

MigrationNeo4jKnowledge Graphs

Concepts mapping

Neo4jFluree
DatabaseLedger (mydb:main)
NodeEntity (identified by IRI, e.g., ex:alice)
Labelrdf:type / @type
PropertyPredicate (e.g., schema:name)
RelationshipObject property (IRI reference to another entity)
Node ID (internal integer)IRI (user-defined, stable, meaningful)
CypherSPARQL 1.1 or JSON-LD Query
APOC proceduresGraph 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.

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

  1. Export Neo4j data as RDF (Neosemantics) or JSON (APOC) and convert to JSON-LD/Turtle.
  2. Start Fluree: docker run -p 8090:8090 fluree/fluree-server:latest
  3. Create a ledger: POST /v1/fluree/create
  4. Load data: POST /v1/fluree/insert or /upsert
  5. Translate Cypher queries to SPARQL or JSON-LD Query using the patterns above.
  6. Verify results.