$npx -y skills add neo4j-contrib/neo4j-skills --skill neo4j-import-skillImport structured data into Neo4j — LOAD CSV, CALL IN TRANSACTIONS, neo4j-admin
| 1 | # Neo4j Import Skill |
| 2 | |
| 3 | ## When to Use |
| 4 | |
| 5 | - Importing CSV, JSON, or Parquet files into Neo4j |
| 6 | - Batch-upserting nodes and relationships (UNWIND + CALL IN TRANSACTIONS) |
| 7 | - Migrating relational data (SQL → graph) |
| 8 | - Bulk-loading large datasets offline (neo4j-admin import) |
| 9 | - Choosing between online (Cypher) and offline (admin) import methods |
| 10 | - Verifying import completeness (counts, constraints, index states) |
| 11 | |
| 12 | ## When NOT to Use |
| 13 | |
| 14 | - **Unstructured docs, PDFs, vector chunks** → `neo4j-document-import-skill` |
| 15 | - **Live application writes (MERGE/CREATE in app code)** → `neo4j-cypher-skill` |
| 16 | - **neo4j-admin backup/restore/config** → `neo4j-cli-tools-skill` |
| 17 | - **GDS algorithm projection from existing graph** → `neo4j-gds-skill` |
| 18 | |
| 19 | --- |
| 20 | |
| 21 | ## Method Decision Table |
| 22 | |
| 23 | | Dataset size | DB state | Source | Method | |
| 24 | |---|---|---|---| |
| 25 | | Any size | Online | CSV (Aura or local) | LOAD CSV + CALL IN TRANSACTIONS | |
| 26 | | < 1M rows | Online | List/API response | UNWIND + CALL IN TRANSACTIONS | |
| 27 | | > 10M rows | **Offline** (local/self-managed) | CSV / Parquet | `neo4j-admin database import full` | |
| 28 | | Any size | Online | APOC available | `apoc.periodic.iterate` + `apoc.load.csv` | |
| 29 | | Any size | Online | JSON/API | `apoc.load.json` or driver batching | |
| 30 | | Incremental delta | Offline (Enterprise) | CSV | `neo4j-admin database import incremental` | |
| 31 | |
| 32 | **Aura**: only `https://` URLs — no `file:///`. Use neo4j-admin import only on self-managed. |
| 33 | |
| 34 | --- |
| 35 | |
| 36 | ## Pre-Import Checklist |
| 37 | |
| 38 | Run in this exact order — skipping causes hard-to-debug duplicates or missed index usage: |
| 39 | |
| 40 | **Constraints BEFORE import. Additional indexes AFTER import.** |
| 41 | - Constraints create implicit RANGE indexes used by MERGE during load + enforce uniqueness |
| 42 | - Additional non-unique indexes (TEXT, RANGE on non-key props, FULLTEXT) created after load — Neo4j populates them async from the committed data; poll `populationPercent` until 100% |
| 43 | - Creating extra indexes before import slows every write during load with no benefit |
| 44 | |
| 45 | 1. **Create uniqueness constraints** (enables index used by MERGE): |
| 46 | ```cypher |
| 47 | CREATE CONSTRAINT IF NOT EXISTS FOR (n:Person) REQUIRE n.id IS UNIQUE; |
| 48 | CREATE CONSTRAINT IF NOT EXISTS FOR (n:Movie) REQUIRE n.movieId IS UNIQUE; |
| 49 | ``` |
| 50 | > **Neo4j 2026.02+ (Enterprise/Aura) — PREVIEW:** `ALTER CURRENT GRAPH TYPE SET { … }` can replace all individual constraint statements with a single declarative block. See `neo4j-cypher-skill/references/graph-type.md`. Use individual `CREATE CONSTRAINT` on older versions or Community Edition. |
| 51 | |
| 52 | 2. **Verify APOC if using apoc.* procedures**: |
| 53 | ```cypher |
| 54 | RETURN apoc.version(); |
| 55 | ``` |
| 56 | If fails → APOC not installed. Use plain LOAD CSV instead. |
| 57 | |
| 58 | 3. **Confirm target is PRIMARY** (not replica): |
| 59 | ```cypher |
| 60 | CALL dbms.cluster.role() YIELD role RETURN role; |
| 61 | ``` |
| 62 | If role ≠ `PRIMARY` → stop. Redirect write to PRIMARY endpoint. |
| 63 | |
| 64 | 4. **Count source file rows** before import (catch encoding issues early): |
| 65 | ```bash |
| 66 | wc -l data/persons.csv # Linux/macOS |
| 67 | ``` |
| 68 | |
| 69 | 5. **Verify UTF-8 encoding** — LOAD CSV requires UTF-8. Re-encode if needed: |
| 70 | ```bash |
| 71 | file -i persons.csv # Check encoding |
| 72 | iconv -f latin1 -t utf-8 persons.csv > persons_utf8.csv |
| 73 | ``` |
| 74 | |
| 75 | --- |
| 76 | |
| 77 | ## LOAD CSV Patterns |
| 78 | |
| 79 | ### Basic node import with type coercion and null handling |
| 80 | |
| 81 | ```cypher |
| 82 | CYPHER 25 |
| 83 | LOAD CSV WITH HEADERS FROM 'file:///persons.csv' AS row |
| 84 | CALL (row) { |
| 85 | MERGE (p:Person {id: row.id}) |
| 86 | ON CREATE SET |
| 87 | p.name = row.name, |
| 88 | p.age = toIntegerOrNull(row.age), |
| 89 | p.score = toFloatOrNull(row.score), |
| 90 | p.active = toBoolean(row.active), |
| 91 | p.born = CASE WHEN row.born IS NOT NULL AND row.born <> '' THEN date(row.born) ELSE null END, |
| 92 | p.createdAt = datetime() |
| 93 | ON MATCH SET |
| 94 | p.updatedAt = datetime() |
| 95 | } IN TRANSACTIONS OF 10000 ROWS |
| 96 | ON ERROR CONTINUE |
| 97 | REPORT STATUS AS s |
| 98 | RETURN s.transactionId, s.committed, s.errorMessage |
| 99 | ``` |
| 100 | |
| 101 | Null/empty-string rules: |
| 102 | - CSV missing column → `null` (safe) |
| 103 | - CSV empty string `""` → stored as `""` **not** `null` — use `nullIf(row.x, '')` to convert |
| 104 | - `toInteger(null)` throws → always use `toIntegerOrNull()` |
| 105 | - `toFloat(null)` throws → always use `toFloatOrNull()` |
| 106 | - Neo4j never stores |