This documentation provides a reference for the structure, nodes, and relationships of the Neo4j graph database implementation of Theographic.
The queries and schema documented here were created and tested on:
- Neo4j Kernel: 2025.10.1 (Enterprise Edition)
- Cypher Language: Cypher 5
For beginners looking to set up their Neo4j environment, please refer to the official Neo4j GraphAcademy or the Neo4j Download Center for installation guides (ex. Desktop, Docker, or AuraDB).
This guide is for manual import that involves running multiple individual files. For automated import, see the Automated Import section below.
To manually run the import scripts, scripts must be executed in the following specific order. While the order of files within a folder (e.g., which node type is imported first) does not matter, the folder order is critical.
- Schema & Indexes: Run
neo4j/import/index.cypherfirst. This sets up constraints and indexes which significantly speed up the subsequent data import. Crucially, this also creates Full-Text Indexes, which enable advanced search capabilities across text fields like names, descriptions, and verse text. - Nodes: Run all scripts in
neo4j/import/nodes/. These create the entities (People, Places, Books, etc.) without connecting them yet. - Relationships: Run all scripts in
neo4j/import/relationships/. These scripts match existing nodes and create the edges (relationships) between them.
⚠️ Important Note: Some of the import scripts may time out. Running with parallel: false (the default setting in the scripts) reduces the chance of failure, but occasional retries and minor troubleshooting may still be necessary. If an import fails, re-run the failed step; repeated runs are safe because the scripts use idempotent patterns (ex. MERGE) and will resume by filling in what's missing.
As an alternative to manually running the Cypher scripts, you can use the provided Python script to automate the entire import process. The script is located at neo4j/import/neo4j_import.py.
-
Python 3.7+: Ensure you have Python installed. You can verify by running:
python --version
-
Neo4j Python Driver: Install the official Neo4j driver:
pip install neo4j
-
Running Neo4j Instance: Make sure your Neo4j database is running and accessible.
Before running the script, open neo4j/import/neo4j_import.py and update the connection settings at the top of the file:
DB_PATH = "bolt://localhost:7687" # Your Neo4j Bolt URI
DB_USER = "neo4j" # Your database username
DB_PASSWORD = "ChangeMyPassword" # Your database password- DB_PATH: The Bolt connection URI. For local installations, this is typically
bolt://localhost:7687. For AuraDB or remote instances, use the connection string provided by your Neo4j service. - DB_USER: Your Neo4j username (default is
neo4j). - DB_PASSWORD: Your Neo4j password. Replace
"ChangeMyPassword"with your actual password.
Navigate to the neo4j/import/ directory and run:
cd neo4j/import
python neo4j_import.pyThe script will automatically:
- Execute the
index.cypherfile to create constraints and indexes - Import all node files from the
nodes/directory - Import all relationship files from the
relationships/directory
Note: The script runs all import operations in the correct order. If any statement fails, it will print an error message but continue with the remaining statements. You can safely re-run the script if needed.
The graph model transforms the flat JSON tables into a connected network of entities. Below is the documentation for each Node Label, its properties, and its relationships.
Represents one of the 66 books of the Protestant canon.
Properties:
| Property | Description |
|---|---|
id |
Unique 14-char identifier |
title |
Full English name of the book |
shortName |
Standard abbreviation (e.g., "Gen") |
osisRef |
OSIS standard identifier (e.g., "Gen") |
slug |
URL-friendly slug |
bookOrder |
Canonical ordering (1-66) |
Relationships:
Note: Arrows (->) indicate the direction of the relationship.
(:Testament)-[:CONTAINS]->(:Book)(:Division)-[:CONTAINS]->(:Book)(:Book)-[:CONTAINS]->(:Chapter)
Represents a distinct individual mentioned in the Bible.
Properties:
| Property | Description |
|---|---|
id |
Unique 14-char identifier |
personId |
Integer ID for external mapping |
name |
Primary name of the person |
alsoCalled |
Alternative names or spellings |
title |
Title or role (e.g., "King of Israel") |
gender |
Gender of the person |
description |
Biographical text from Easton's Dictionary |
slug |
URL-friendly slug |
Relationships:
(:Person)-[:CHILD_OF]->(:Person)(:Person)-[:PARTNER_OF]->(:Person)(:Person)-[:BORN_IN]->(:Place)(:Person)-[:DIED_IN]->(:Place)(:Person)-[:BORN_IN]->(:Year)(:Person)-[:DIED_IN]->(:Year)(:Verse)-[:MENTIONS]->(:Person)
Represents a geographic location mentioned in the text.
Properties:
| Property | Description |
|---|---|
id |
Unique 14-char identifier |
placeId |
Integer ID for external mapping |
name |
Display name of the place |
latitude |
GPS Latitude |
longitude |
GPS Longitude |
featureType |
Type of location (e.g., "City", "Mountain") |
description |
Description from dictionary |
comment |
Editor's comments or notes |
precision |
Accuracy of the geolocation |
Relationships:
(:Event)-[:OCCURRED_IN]->(:Place)(:Person)-[:BORN_IN]->(:Place)(:Person)-[:DIED_IN]->(:Place)(:Verse)-[:MENTIONS]->(:Place)
Represents a specific event or narrative unit in the biblical timeline.
Properties:
| Property | Description |
|---|---|
id |
Unique 14-char identifier |
title |
Title of the event |
startDate |
Approximate start date |
duration |
Duration of the event |
sortKey |
Chronological sorting key |
Relationships:
(:Event)-[:PRECEEDS]->(:Event)(:Person)-[:PARTICIPATED_IN]->(:Event)(:Event)-[:OCCURRED_IN]->(:Place)(:Verse)-[:DESCRIBES]->(:Event)
Represents a single verse of scripture.
Properties:
| Property | Description |
|---|---|
id |
Unique 14-char identifier |
osisRef |
Standard reference (e.g., "Gen.1.1") |
verseText |
The text of the verse |
verseNum |
Verse number |
Relationships:
(:Chapter)-[:CONTAINS]->(:Verse)(:Verse)-[:MENTIONS]->(:Person)(:Verse)-[:MENTIONS]->(:Place)(:Verse)-[:DESCRIBES]->(:Event)
Represents a chapter within a book.
Properties:
| Property | Description |
|---|---|
id |
Unique 14-char identifier |
chapterNum |
Chapter number |
osisRef |
Standard reference (e.g., "Gen.1") |
Relationships:
(:Book)-[:CONTAINS]->(:Chapter)(:Chapter)-[:CONTAINS]->(:Verse)
Represents a specific year in history.
Properties:
| Property | Description |
|---|---|
id |
Unique 14-char identifier |
year |
Numeric year (negative for BC) |
formattedYear |
Display string (e.g., "1000 BC") |
Relationships:
(:Person)-[:BORN_IN]->(:Year)(:Person)-[:DIED_IN]->(:Year)
Represents a group of people (e.g., "Israelites", "Pharisees").
Properties:
| Property | Description |
|---|---|
id |
Unique 14-char identifier |
name |
Name of the group |
Relationships:
(:Person)-[:BELONGS_TO]->(:PeopleGroup)
Represents an entry from Easton's Bible Dictionary.
Properties:
| Property | Description |
|---|---|
id |
Unique 14-char identifier |
term |
The dictionary term |
dictText |
The definition text |
Relationships:
(:Person)-[:DEFINED_BY]->(:Dictionary)(:Place)-[:DEFINED_BY]->(:Dictionary)
This section demonstrates example cases of how to use the graph.
Find the father and grandfather of King David:
MATCH (david:Person {name: "David"})-[:CHILD_OF]->(father)-[:CHILD_OF]->(grandfather)
RETURN father.name, grandfather.nameFind the siblings of Moses:
MATCH (p1:Person)-[:CHILD_OF]->(parent)<-[:CHILD_OF]-(p2:Person)
WHERE p1.name = "Moses" AND p1 <> p2
RETURN DISTINCT p2.name AS SiblingFind all events that happened in Jerusalem:
MATCH (e:Event)-[:OCCURRED_IN]->(p:Place {name: "Jerusalem"})
RETURN e.title, e.startDate
ORDER BY e.sortKeyFind the place where Paul was born:
MATCH (paul:Person {name: "Paul"})-[:BORN_IN]->(p:Place)
RETURN paul.name, p.name, p.latitude, p.longitude;Find which events Moses was part of:
MATCH (p:Person {name: "Moses"})-[:PARTICIPATED_IN]->(e:Event)
RETURN e.title, e.startDate
ORDER BY e.sortKey ASC
LIMIT 10Find who participated in the event called "The Great Flood begins":
MATCH (e:Event {title: "The Great Flood begins"})<-[:PARTICIPATED_IN]-(p:Person)
RETURN p.nameIf you ran the index.cypher file during setup, you have access to powerful full-text search capabilities. This allows you to do things like:
- Phrase Search: Find exact phrases like "River of Egypt".
- Boolean Search: Combine required/optional/excluded terms with Lucene operators like AND, OR, and NOT.
- Proximity Search: Find words that are near each other (e.g., "love" within 5 words of "hate").
- Fielded Search: Search specific properties (e.g.,
title:Prophet). - Range Search: Find values within a range (e.g., years
[1000 TO 1010]). - Weighted Search (Boosting): Boost the relevance of certain terms or fields (e.g.,
title:king AND title:Israel^2).
The results will be ranked by how well they match the query.
Example 1: Boolean Search
Find people associated with both the terms "Prophet" and "Judge".
CALL db.index.fulltext.queryNodes("person_fulltext", "Prophet AND Judge")
YIELD node, score
RETURN node.name, node.title, score LIMIT 5;Returns:
| node.name | node.title | score |
|---|---|---|
| Jesus | Jesus Christ | 2.17 |
| Samuel | Samuel | 1.74 |
Example 2: Proximity Search
Find verses where "love" and "hate" appear within 5 words of each other.
CALL db.index.fulltext.queryNodes("verse_fulltext", "\"love hate\"~5")
YIELD node, score
RETURN node.osisRef, node.verseText, score LIMIT 3;Returns:
| node.osisRef | node.verseText | score |
|---|---|---|
| Ps.97.10 | Ye that love the LORD, hate evil... | 2.38 |
| Prov.8.36 | ...all they that hate me love death. | 2.30 |
| Matt.5.43 | ...Thou shalt love thy neighbour, and hate thine enemy. | 2.30 |
Example 3: Fielded Search
Search only within the title property for "King".
CALL db.index.fulltext.queryNodes("person_fulltext", "title:King")
YIELD node, score
RETURN node.name, node.title, score LIMIT 3;Returns:
| node.name | node.title | score |
|---|---|---|
| Abimelech | Abimelech (King of Gerar) | 1.50 |
| Arioch | Arioch (king of Ellasar) | 1.50 |
| Jabin | Jabin (king of Hazor) | 1.50 |
Example 4: Range Search
Find years between 1000 BC and 1010 BC.
CALL db.index.fulltext.queryNodes("year_fulltext", "[1000 TO 1010]")
YIELD node, score
RETURN node.formattedYear, score;Returns:
| node.formattedYear | score |
|---|---|
| 1006 BC | 1.0 |
| 1001 BC | 1.0 |
| 1004 BC | 1.0 |
Example 5: Weighted Search (Boosting)
Find multiple figures called Joseph, but order them based on if their title contains "son of" or not.
CALL db.index.fulltext.queryNodes(
"person_fulltext",
'name:joseph^3, title:"son of"^2'
)
YIELD node, score
RETURN node.name, node.title, score ORDER BY score DESC LIMIT 4;Returns:
| node.name | node.title | score |
|---|---|---|
| Joseph | Joseph (son of Jacob) | 11.35 |
| Joseph | Joseph (son of Asaph) | 11.35 |
| Joseph | Joseph (son of Judah) | 11.35 |
| Joseph | Joseph | 7.86 |