Graphing with Crux
Remix (fork) this in Nextjournal to get started with your own Crux notebook.
Introduction
Datalog is a powerful query interface that allows you to elegant express queries to retrieve information from the graphs of information contained across your documents.
"Imagine a user being part of different groups. A group can have different roles, and a user can be part of different groups. He also can have different roles in different groups apart from the membership. The association of a User, a Group and a Role can be referred to as a HyperEdge. However, it can be easily modeled in a property graph as a node that captures this n-ary relationship..."
This notebook example is originally based on data and queries from the Neo4J Cookbook, where a visual illustration of the sample graph can be viewed: https://neo4j.com/docs/stable/cypher-cookbook-hyperedges.html
{:deps {org.clojure/clojure {:mvn/version "1.10.0"} org.clojure/tools.deps.alpha {:git/url "https://github.com/clojure/tools.deps.alpha.git" :sha "f6c080bd0049211021ea59e516d1785b08302515"} juxt/crux-core {:mvn/version "RELEASE"} ; "RELEASE" is the latest non-snapshot version from Clojars juxt/crux-decorators {:mvn/version "RELEASE"}}}
(require [crux.api :as crux] [crux.decorators.aggregation.alpha :as aggr])
Create a Crux node
(def db-node (crux/start-standalone-node {:kv-backend "crux.kv.memdb.MemKv" :db-dir "data/db-dir-1" :event-log-dir "data/event-log-dir-1"})) ; alternatively, you can go with RocksDB for a persistent storage (comment juxt/crux-rocksdb {:mvn/version "RELEASE"} ; add this to your deps ; define node as follows (def node (crux/start-standalone-node ; it has clustering out-of-the-box though {:kv-backend "crux.kv.rocksdb.RocksKv" :db-dir "data/db-dir-1"})))
Define a graph
(def nodes (for [n [{:user/name :User1 :hasRoleInGroups {:U1G3R34 :U1G2R23}} {:user/name :User2 :hasRoleInGroups {:U2G2R34 :U2G3R56 :U2G1R25}} {:role/name :Role1} {:role/name :Role2} {:role/name :Role3} {:role/name :Role4} {:role/name :Role5} {:role/name :Role6} {:group/name :Group1} {:group/name :Group2} {:group/name :Group3} {:roleInGroup/name :U2G2R34 :hasGroups {:Group2} :hasRoles {:Role3 :Role4}} {:roleInGroup/name :U1G2R23 :hasGroups {:Group2} :hasRoles {:Role2 :Role3}} {:roleInGroup/name :U1G3R34 :hasGroups {:Group3} :hasRoles {:Role3 :Role4}} {:roleInGroup/name :U2G3R56 :hasGroups {:Group3} :hasRoles {:Role5 :Role6}} {:roleInGroup/name :U2G1R25 :hasGroups {:Group1} :hasRoles {:Role2 :Role5}} {:roleInGroup/name :U1G1R12 :hasGroups {:Group1} :hasRoles {:Role1 :Role2}}]] (assoc n :crux.db/id (get n (some {:user/name :group/name :role/name :roleInGroup/name} (keys n)))))) (crux/submit-tx db-node (mapv (fn [n] [:crux.tx/put n]) nodes))
Find roles for user and particular groups
(def db (crux/db db-node)) (crux/q db {:find [?roleName] :where [[?e :hasRoleInGroups ?roleInGroup] [?roleInGroup :hasGroups ?group] [?roleInGroup :hasRoles ?role] [?role :role/name ?roleName]] :args [{?e :User1 ?group :Group2}]})
Find all groups and roles for a user
(crux/q db {:find [?groupName ?roleName] :where [[?e :hasRoleInGroups ?roleInGroup] [?roleInGroup :hasGroups ?group] [?group :group/name ?groupName] [?roleInGroup :hasRoles ?role] [?role :role/name ?roleName]] :args [{?e :User2}]})
Define a Datalog Rule
;; a datalog rule (def rules [[(user-roles-in-groups ?user ?role ?group) [?user :hasRoleInGroups ?roleInGroup] [?roleInGroup :hasGroups ?group] [?roleInGroup :hasRoles ?role]]])
Find all groups and roles for a user, using a Datalog rule
(crux/q db {:find [?groupName ?roleName] :where [(user-roles-in-groups ?user ?role ?group) [?group :group/name ?groupName] [?role :role/name ?roleName]] :rules rules :args [{?user :User1}]})
Find common groups based on shared roles and count the number of shared roles
This uses the aggregation decorator, which wraps the default `crux/q` and looks for `:aggr` instead of `:find`
(aggr/q db {:aggr {:partition-by [?groupName] :select {?roleCount [0 (inc acc) ?role]}} :where [(user-roles-in-groups ?user1 ?role ?group) (user-roles-in-groups ?user2 ?role ?group) [?group :group/name ?groupName]] :rules rules :args [{?user1 :User1 ?user2 :User2}]})
What's next?
Try adding additional :hasRoleInGroups values (e.g. `#{:U1G1R12 :U2G3R56 :U2G1R25}`) to :User1 by submitting a new version of the document