FalkorDB Graph Model
FalkorDB is not a cache or secondary store — it is the product. The entire scripture knowledge graph lives here: passages, topics, lexicon entries, manuscript witnesses, cross-references, and the relationships between them.
Why FalkorDB?
- Redis-based — operational simplicity; same protocol and tooling as Redis
- Cypher-compatible — standard graph query language
- Performance — in-memory graph traversal, sub-millisecond for 2-3 hop queries
- Chosen over Neo4j (licensing cost, ops complexity) and ArangoDB (less mature Cypher support)
The 7 Schema Families
Every JSON file in the corpus declares its type via a schema field. No heuristic detection is needed.
| Schema | File Pattern | Description |
|---|---|---|
scripture-text | corpus/{bookId}.json | Passages with optional witnesses, words, notes |
lexicon | lexicon/{range}.json | Strong's Hebrew/Greek dictionary entries |
topical-guide | tg/{letter}.json | LDS Topical Guide topics |
bible-dictionary | bd/{letter}.json | LDS Bible Dictionary articles |
scripture-index | index/{letter}.json | Cross-reference index |
verse-commentary | commentary/{id}/{bookId}.json | Verse-level scholarly commentary |
scholarly-commentary | scholarly/{id}.json | Section-level scholarly commentary |
Node Types
graph LR
P["Passage"] --- W["Witness"]
P --- WA["WordAlignment"]
P --- N["VerseNote"]
P --- T["IndexTopic"]
T --- BD["BDArticle"]
WA --- L["LexiconEntry"]
| Node Label | Key Properties | Example |
|---|---|---|
Passage | id, text, book_id, chapter, verse, corpus | gen.1.1 |
Witness | language, script, text, witness, edition | Ethiopic text of 1 Enoch |
WordAlignment | order, gloss, strongs, token | Hebrew בְּרֵאשִׁ֖ית → "In the beginning" |
LexiconEntry | id, lemma, language, strongs_id, gloss, definition | H7225 → רֵאשִׁית |
IndexTopic | id, title, description | tg:angels |
BDArticle | id, title, body | bd:aaron |
VerseNote | anchor, content | Footnote on Gen 1:1 |
Edge Types
| Edge | From → To | Purpose |
|---|---|---|
HAS_ORIGINAL | Passage → Witness | Links passage to manuscript witness |
CROSS_REF | Passage → Passage | Scripture cross-reference |
CITES | IndexTopic → Passage | Topic cites a passage |
SEE_ALSO_TG | IndexTopic → IndexTopic | Related topic link |
SEE_ALSO_BD | IndexTopic → BDArticle | Topic → dictionary article |
HAS_WORD | Passage → WordAlignment | Interlinear word data |
DEFINED_BY | WordAlignment → LexiconEntry | Word → lexicon definition |
HAS_NOTE | Passage → VerseNote | Scholarly footnote |
Shared Types
PassageRef
The canonical representation of a location reference:
interface PassageRef {
bookId: string // "gen", "1-enoch", "dc"
chapter: number // 1-based
verse?: number // 1-based; absent for chapter-only refs
verseEnd?: number // Inclusive end: verse 3–7 → verse:3, verseEnd:7
chapterEnd?: number // Cross-chapter ranges
}
SeeAlsoLink (Discriminated Union)
Every cross-reference has an explicit type field — no positional inference:
| Type | ID Format | Example |
|---|---|---|
topic | tg:angels | Link to Topical Guide entry |
article | bd:angels | Link to Bible Dictionary entry |
passage | PassageRef object | Cross-reference to another verse |
person | person:aaron.1 | Link to person node |
place | place:ammonihah | Link to place node |
Display names (title/name) are denormalized on non-passage links for rendering without graph lookups.
Cypher Query Patterns
Passage Retrieval
MATCH (p:Passage {id: $passage_id})
OPTIONAL MATCH (p)-[:HAS_ORIGINAL]->(witness:Witness)
OPTIONAL MATCH (p)-[:CROSS_REF]->(ref:Passage)
WITH p, collect(DISTINCT witness) AS witnesses, collect(DISTINCT ref) AS refs
RETURN p, witnesses, refs
Topic Subgraph
MATCH (t:IndexTopic {id: $topic_id})
CALL {
WITH t
MATCH (t)-[:CITES]->(p:Passage)
RETURN 'passage' AS type, p AS node
UNION
WITH t
MATCH (t)-[:SEE_ALSO_TG]->(related:IndexTopic)
RETURN 'topic' AS type, related AS node
}
RETURN type, node
LIMIT $limit
Connection Discovery
MATCH (p:Passage {id: $passage_id})
MATCH (p)-[rel]->(connected)
RETURN type(rel) AS relationship_type,
labels(connected)[0] AS node_type,
connected.id AS connected_id,
connected.title AS connected_title,
rel.weight AS weight
ORDER BY rel.weight DESC
LIMIT $limit
MERGE Idempotency
All graph writes use MERGE (never CREATE) to ensure idempotency. The ingest pipeline can be re-run safely without creating duplicate nodes.
-- Idempotent node creation
MERGE (p:Passage {id: $id})
SET p.text = $text, p.book_id = $book_id, p.chapter = $chapter, p.verse = $verse
-- Idempotent edge creation
MATCH (a:Passage {id: $from_id}), (b:Passage {id: $to_id})
MERGE (a)-[:CROSS_REF]->(b)
Batch writes use UNWIND for performance:
UNWIND $passages AS p
MERGE (node:Passage {id: p.id})
SET node += p
Design Principles
- Every file declares its own type via the
schemafield - All cross-references are structured objects (PassageRef), not human-formatted strings
bookIdis always a canonical slug — no integers, no display titles- Optional fields are absent, not null or empty string
- camelCase throughout — no mixed conventions
- Presentation concerns excluded — no page numbers, URL slugs, or typesetting tokens
Related Pages
- Data Architecture Overview — When to use which store
- PostgreSQL Schema — Operational data that doesn't belong in the graph
- Typesense Search — Full-text search index synced from FalkorDB
- Data Sources — Corpus sources that populate the graph