Rayforce ← Back to home
GitHub

Building a Knowledge Base with Datalog

Model entities and relationships as triples, write rules, and query with recursive logic — all within the Rayfall REPL.

Rayforce includes a built-in Datalog engine that stores facts as entity-attribute-value (EAV) triples and evaluates rules to fixpoint. This guide walks through building a practical knowledge base from scratch.

1. Modeling Data as Triples

A Datalog database stores facts as (entity, attribute, value) triples. Create a database with datoms and add facts with assert-fact:

; Create an empty Datalog database
(set db (datoms))

; Assert facts about entities
(set db (assert-fact db 1 'name 'Alice))
(set db (assert-fact db 1 'role 'Engineer))
(set db (assert-fact db 1 'dept 'Platform))
(set db (assert-fact db 2 'name 'Bob))
(set db (assert-fact db 2 'role 'Manager))
(set db (assert-fact db 2 'dept 'Platform))
(set db (assert-fact db 3 'name 'Charlie))
(set db (assert-fact db 3 'role 'Engineer))
(set db (assert-fact db 3 'dept 'Frontend))

Each assert-fact call adds one triple: (entity-id, attribute, value). Entity IDs are integers you assign. Attributes and values are symbols.

EAV vs Regular Tables

Use EAV (Datalog) when your data is schema-flexible — entities can have different attributes, and you need rule-based reasoning. Use regular tables when your data has a fixed schema and you need fast columnar analytics.

2. Querying Relationships

Use query with find and where clauses. Variables start with ? and are bound by pattern matching across triples:

; Find all names
(query db (find ?e ?n) (where (?e :name ?n)))
┌─────┬───────────────────────────────┐
│ ?e  │              ?n               │
│ i64 │              i64              │
├─────┼───────────────────────────────┤
│ 1   │ 158                           │
│ 2   │ 163                           │
│ 3   │ 165                           │
├─────┴───────────────────────────────┤
│ 3 rows (3 shown) 2 columns (2 shown)│
└─────────────────────────────────────┘

The value column shows internal symbol IDs (integers). Use sym-name to convert them to readable strings (see Section 7). You can join across multiple patterns to find related facts:

; Find name and department for each person
(query db (find ?n ?d) (where (?e :name ?n) (?e :dept ?d)))
┌─────┬───────────────────────────────┐
│ ?n  │              ?d               │
│ i64 │              i64              │
├─────┼───────────────────────────────┤
│ 158 │ 162                           │
│ 163 │ 162                           │
│ 165 │ 166                           │
├─────┴───────────────────────────────┤
│ 3 rows (3 shown) 2 columns (2 shown)│
└─────────────────────────────────────┘

Filter by constant values in patterns:

; Find only Engineers
(query db (find ?n) (where (?e :name ?n) (?e :role 'Engineer)))
┌─────────────────────────────────────┐
│                 ?n                  │
│                 i64                 │
├─────────────────────────────────────┤
│ 158                                 │
│ 165                                 │
├─────────────────────────────────────┤
│ 2 rows (2 shown) 1 columns (1 shown)│
└─────────────────────────────────────┘

3. Writing Rules

Rules define derived relations. The pattern is: “if these patterns match, then this relation holds.”

; Define a "team-member" rule
(rule (team-member ?name ?dept)
  (?e :name ?name)
  (?e :dept ?dept))

; Use the rule in a query
(query db (find ?name ?dept) (where (team-member ?name ?dept)))
┌───────┬─────────────────────────────┐
│ ?name │            ?dept            │
│  i64  │             i64             │
├───────┼─────────────────────────────┤
│ 158   │ 162                         │
│ 163   │ 162                         │
│ 165   │ 166                         │
├───────┴─────────────────────────────┤
│ 3 rows (3 shown) 2 columns (2 shown)│
└─────────────────────────────────────┘

Rules act like views — they are re-evaluated each time they appear in a query. Rules can reference other rules, enabling modular knowledge bases.

4. Recursive Queries

Rules can be recursive, enabling transitive closure. Here we model a management hierarchy where entity 1 manages 2, 2 manages 3, and 3 manages 4:

(set db (datoms))
(set db (assert-fact db 1 'name 'Alice))
(set db (assert-fact db 2 'name 'Bob))
(set db (assert-fact db 3 'name 'Charlie))
(set db (assert-fact db 4 'name 'Diana))
(set db (assert-fact db 1 'manages 2))
(set db (assert-fact db 2 'manages 3))
(set db (assert-fact db 3 'manages 4))

; Base case: direct management
(rule (reports-to ?x ?y) (?x :manages ?y))
; Recursive case: transitive closure
(rule (reports-to ?x ?z) (?x :manages ?y) (reports-to ?y ?z))

(query db (find ?x ?y) (where (reports-to ?x ?y)))
┌─────┬───────────────────────────────┐
│ ?x  │              ?y               │
│ i64 │              i64              │
├─────┼───────────────────────────────┤
│ 1   │ 2                             │
│ 1   │ 3                             │
│ 2   │ 3                             │
│ 2   │ 4                             │
│ 3   │ 4                             │
│ 1   │ 4                             │
├─────┴───────────────────────────────┤
│ 6 rows (6 shown) 2 columns (2 shown)│
└─────────────────────────────────────┘

Entity 1 (Alice) reaches all others transitively — including entity 4 (Diana) who is three levels down.

5. Negation

Use not in a where clause to find entities that lack a given attribute:

(set db (datoms))
(set db (assert-fact db 1 'name 'Alice))
(set db (assert-fact db 2 'name 'Bob))
(set db (assert-fact db 3 'name 'Charlie))
(set db (assert-fact db 1 'certified 'true))
(set db (assert-fact db 3 'certified 'true))

; Find people WITHOUT certification
(query db (find ?n) (where (?e :name ?n) (not (?e :certified ?c))))
┌─────────────────────────────────────┐
│                 ?n                  │
│                 i64                 │
├─────────────────────────────────────┤
│ 161                                 │
├─────────────────────────────────────┤
│ 1 rows (1 shown) 1 columns (1 shown)│
└─────────────────────────────────────┘

Only Bob (sym ID 161) lacks a certified attribute.

6. Retracting Facts

Remove facts with retract-fact:

; Remove Bob's name fact
(set db (retract-fact db 2 'name 'Bob))

; Query again — Bob is gone
(query db (find ?e ?n) (where (?e :name ?n)))

The database is immutable in the functional sense — retract-fact returns a new database value, so previous versions are unaffected if you kept a reference.

7. Reading Sym IDs

Query results show raw symbol IDs (integers) for string values. Use sym-name to convert an ID back to its symbol name, and pull to retrieve all attributes of an entity:

; Pull all attributes for entity 1
(pull db 1)
['name 158 'role 160 'dept 162]
; Pull specific attributes
(pull db 1 [name role])
['name 158 'role 160]
; Convert a sym ID to its name
(sym-name 158)
'Alice

The pull function returns a flat list of alternating attribute-symbol and value pairs. For string-valued attributes, the value is a symbol ID that you decode with sym-name.

8. Real-World Example: Org Chart

Let’s build a complete org chart with a CEO, VPs, and team leads:

(set db (datoms))

; CEO
(set db (assert-fact db 1 'name 'Elena))
(set db (assert-fact db 1 'title 'CEO))

; VPs
(set db (assert-fact db 2 'name 'Frank))
(set db (assert-fact db 2 'title 'VP_Engineering))
(set db (assert-fact db 3 'name 'Grace))
(set db (assert-fact db 3 'title 'VP_Sales))

; Team leads
(set db (assert-fact db 4 'name 'Henry))
(set db (assert-fact db 4 'title 'Tech_Lead))
(set db (assert-fact db 5 'name 'Iris))
(set db (assert-fact db 5 'title 'Sales_Lead))

; Reporting structure
(set db (assert-fact db 1 'manages 2))
(set db (assert-fact db 1 'manages 3))
(set db (assert-fact db 2 'manages 4))
(set db (assert-fact db 3 'manages 5))

; Rules
(rule (chain-of-command ?top ?bottom)
  (?top :manages ?bottom))
(rule (chain-of-command ?top ?bottom)
  (?top :manages ?mid)
  (chain-of-command ?mid ?bottom))

Direct reports:

(query db (find ?mgr ?emp)
  (where (?mgr :manages ?emp)))
┌──────┬──────────────────────────────┐
│ ?mgr │             ?emp             │
│ i64  │             i64              │
├──────┼──────────────────────────────┤
│ 1    │ 2                            │
│ 1    │ 3                            │
│ 2    │ 4                            │
│ 3    │ 5                            │
├──────┴──────────────────────────────┤
│ 4 rows (4 shown) 2 columns (2 shown)│
└─────────────────────────────────────┘

Full chain of command (transitive closure):

(query db (find ?top ?bottom)
  (where (chain-of-command ?top ?bottom)))
┌──────┬──────────────────────────────┐
│ ?top │           ?bottom            │
│ i64  │             i64              │
├──────┼──────────────────────────────┤
│ 1    │ 2                            │
│ 1    │ 3                            │
│ 1    │ 4                            │
│ 1    │ 5                            │
│ 2    │ 4                            │
│ 3    │ 5                            │
├──────┴──────────────────────────────┤
│ 6 rows (6 shown) 2 columns (2 shown)│
└─────────────────────────────────────┘

Who does the CEO (entity 1) manage, directly or indirectly?

(query db (find ?emp) (where (chain-of-command 1 ?emp)))
┌─────────────────────────────────────┐
│                ?emp                 │
│                 i64                 │
├─────────────────────────────────────┤
│ 2                                   │
│ 3                                   │
│ 4                                   │
│ 5                                   │
├─────────────────────────────────────┤
│ 4 rows (4 shown) 1 columns (1 shown)│
└─────────────────────────────────────┘

The CEO reaches all four employees through the recursive chain-of-command rule.

Next Steps